	(function() {
	    const maxSendBlock = 24;
	    var async = require('async');
	    var request = require('request');
		// require for fabric client
		var log4js = require('log4js');
		var util = require('util');
		var logger = log4js.getLogger('explorer');
		var config = require('./config.json');
		var helper = require('./helper.js');
		var query = require('./query.js');
		var grpc = require('grpc')
		var path = require('path')
		var protos = grpc.load(path.join(__dirname,
		 '../node_modules/fabric-client/lib/protos/peer/proposal.proto')).protos;
		var readStringFromBB = function(bb) {
			return bb.buffer.toString("utf8", bb.offset, bb.limit);
		}
		var readHexFromBB = function(bb) {
			return bb.buffer.toString("hex", bb.offset, bb.limit);
		}
		var convertLedgerHeightToLong = function(h) {
			return (h.unsigned? 1:-1) * (h.high << 32 | h.low);
		}
	    var ledgerData;
	    var loaded = false;

		// read config from
		if (process.env.CHAINCODE_ID) {
			config.chaincodeId = process.env.CHAINCODE_ID;
		}
		if (process.env.CHANNEL_NAME) {
			config.channelName = process.env.CHANNEL_NAME;
		}

	    var getChain = function(callback) {
			logger.debug("try to get chain info");
	        // callFabric('/chain', callback);
			return query.getChainInfo(
				config.peerName, 
				config.userName, 
				config.orgName)
				.then( ci => {
 					return Promise.resolve({
 						height: convertLedgerHeightToLong(ci.height),
 						currentBlockHash: readHexFromBB(ci.currentBlockHash),
 						previousBlockHash: readHexFromBB(ci.previousBlockHash)
					}); 
				})
				.then(callback)
				.catch(err => logger.error(err));
	    }

	    var getPeers = function(callback) {
			logger.debug("try to get peer info");
	        // callFabric('/network/peers', callback);
			let grpcAddress = process.env.PEER_ADDR || 'grpc://localhost:7051';
			var peers = [{name: config.peerName, address: grpcAddress, type: 'peer', pkiID:config.orgName}];
			callback(peers);
	    }

	    var getBlock = function(blockNum, callback) {
	        // callFabric('/chain/blocks/' + blockNum, callback);
			logger.debug(util.format("try to get block data for block#:%d", blockNum))
			return query.getBlockByNumber(
				config.peerName, 
				blockNum, 
				config.userName,
				config.orgName)
				.then( bi => {
					let transactions = []
					let td = bi.data.data;

					logger.debug(util.inspect(td, false, null));
					let blockTimestamp = null
					for(let i = 0; i < td.length; ++i) {
						// skip CONFIG transaction
						if (td[i].payload.header.channel_header.type == 1) continue;
						var proposal = protos.Proposal.decode(td[i].payload.data.actions[0].payload.chaincode_proposal_payload);
						var ccpp = protos.ChaincodeProposalPayload.decode(proposal.header);
						var ccspec = protos.ChaincodeSpec.decode(ccpp.input);
						var args = []
						for(let i = 0; i < ccspec.input.args.length ; ++i) {
				            let bb = ccspec.input.args[i];
				            // !!! not easy to figure out
						    args.push(bb.buffer.toString("utf8", bb.offset, bb.limit));
						}
						transactions.push( {
							signature: td[i].signature,
							channelId: td[i].payload.header.channel_header.channel_id,
							timestamp: td[i].payload.header.channel_header.timestamp,
							chaincodeId: protos.ChaincodeHeaderExtension.decode(new Buffer(td[i].payload.header.channel_header.extension, 'hex')).chaincode_id.name,
							txId: td[i].payload.header.channel_header.tx_id,
							args: args
						});
						// FIXME: better use most recent timestamp
						if (! blockTimestamp) {
							blockTimestamp = td[i].payload.header.channel_header.timestamp
						}
					}
					return Promise.resolve( {
						blockNum: convertLedgerHeightToLong(bi.header.number),
						stateHash: bi.header.data_hash,
						previousBlockHash: bi.header.previous_hash,
						timestamp: blockTimestamp,
						transactions: transactions
					})
				})
				.then(callback)
				.catch(err => logger.error(err));
	    }

	    var updateLedgerData = function(callback) {
	        getChain(function(data) {
	            if (ledgerData && ledgerData.chain.height === data.height) {
	                callback(false, ledgerData);
	            } else {
	                var preHeight = ledgerData ? ledgerData.chain.height : 0;
	                var curHeight = data.height;
	                if (!ledgerData) {
	                    // NOTE: init here
	                    ledgerData = {};
	                    ledgerData.blocks = []
	                }
	                ledgerData.chain = data;
	                getPeers(function(data) {
	                    ledgerData.peers = data;
	                    // shift as we only need maxSendBlock
	                    var shift = curHeight <= maxSendBlock ?
	                        0 : (curHeight - maxSendBlock);

	                    // overwrite the latter part for new blocks
	                    // TODO: consider no overlapping
	                    var start = shift;
	                    if (shift > 0 && preHeight != 0 && curHeight - preHeight < maxSendBlock) {
	                        for (var i = 0; i < maxSendBlock - (curHeight - preHeight); ++i) {
	                            ledgerData.blocks[i] = ledgerData.blocks[i + curHeight - preHeight];
	                        }
	                        start += maxSendBlock - (curHeight - preHeight);
	                    }

	                    var blockFunc = function(data) {
							logger.debug("got block data for #%d", data.blockNum); 
	                        ledgerData.blocks[start - shift] = data;
							// FIXME: start is not good defined
	                        if (start >= ledgerData.chain.height - 1 || (start - shift) == maxSendBlock - 1) {
	                            loaded = true;
								logger.debug("********ledger data***********");
								// logger.debug(ledgerData);
	                            callback(true, ledgerData);
	                        } else getBlock((++start), blockFunc);
	                    }
	                    if (preHeight != ledgerData.chain.height) {
							logger.debug(
								util.format("prevHeight=%d, current ledger data=%j. start retrieving new block from#%d.",
								preHeight, ledgerData.chain, start));
	                        getBlock(start, blockFunc);
						}
	                });
	            }
	        });
	    }

	    module.exports.ledgerData = function() { return ledgerData; }
	    module.exports.update = updateLedgerData;
	    module.exports.loaded = function() { return loaded; }
		module.exports.logger = logger
	}());